﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using Microsoft.Surface.Presentation;
using System.Collections;
using System.Windows.Media.Animation;
using System.Windows.Input;
using System.Windows.Controls.Primitives;
using System.Collections.ObjectModel;
using Microsoft.Surface.Presentation.Controls;

namespace WPFMitsuControlsv2
{
    [TemplatePart(Name = Book.ElementSheetLeft, Type = typeof(ContentPresenter))]
    [TemplatePart(Name = Book.ElementSheetRight, Type = typeof(ContentPresenter))]
    [TemplatePart(Name = Book.ElementRoot, Type = typeof(Grid))]
    [TemplatePart(Name = Book.ElementBookmarker, Type = typeof(Border))]
    [TemplatePart(Name = Book.ElementBookmarksScrollViewer, Type = typeof(SurfaceScrollViewer))]
    [StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(BookItem))]
    public partial class Book : SurfaceListBox
    {

        #region Constants

        private const string ElementRoot = "PART_Root";
        private const string ElementSheetLeft = "PART_SheetLeft";
        private const string ElementSheetRight = "PART_SheetRight";
        private const string ElementBookmarker = "PART_Bookmarker";
        private const string ElementBookmarksScrollViewer = "BookmarksScrollViewer";

        // animations durations
        private readonly Duration BookmarkerTranslateDuration = new Duration(TimeSpan.FromMilliseconds(800));
        private readonly Duration BookmarkerFadeDuration = new Duration(TimeSpan.FromMilliseconds(400));

        internal const double DefaultGripSize = 40d;

        #endregion Constants

        #region Private members

        private ScaleTransform _scale;
        private TranslateTransform _translate;
        private PageStatus _status = PageStatus.None;
        private int _currentSheetIndex = 0;
        private Grid _rootGrid;
        private ContentPresenter _sheetLeftPresenter;
        private ContentPresenter _sheetRightPresenter;
        private ContentControl _bookmarker;
        private SurfaceScrollViewer _bookmarksSV;

        #endregion

        #region Constructors

        public Book()
        {
            SelectedIndex = 0;
            this.Loaded += OnLoaded;
        }

        static Book()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Book), new FrameworkPropertyMetadata(typeof(Book)));

            DisplayModeProperty = DependencyProperty.Register("DisplayMode", typeof(BookDisplayMode), typeof(Book),
                new PropertyMetadata(BookDisplayMode.Normal, new PropertyChangedCallback(OnDisplayModeChanged)));
            CurrentPageProperty = DependencyProperty.Register("CurrentPage", typeof(BookCurrentPage), typeof(Book),
                new PropertyMetadata(BookCurrentPage.RightSheet, new PropertyChangedCallback(OnCurrentPageChanged)));
        }

        #endregion

        #region Public properties

        public int LeftPageIndex
        {
            get
            {
                return this.SelectedIndex % 2 == 0 ?
                    this.SelectedIndex - 1 : this.SelectedIndex;
            }
        }

        public int RightPageIndex
        {
            get
            {
                return this.SelectedIndex % 2 == 0 ?
                    this.SelectedIndex : this.SelectedIndex + 1;
            }
        }

        public static DependencyProperty DisplayModeProperty;
        public static DependencyProperty CurrentPageProperty;


        public Size? PageSize
        {
            get { return (Size?)GetValue(PageSizeProperty); }
            set { SetValue(PageSizeProperty, value); }
        }

        public static readonly DependencyProperty PageSizeProperty =
            DependencyProperty.Register("PageSize", typeof(Size?), typeof(Book), new UIPropertyMetadata(null, OnPageSizeChanged));


        public Visibility BookmarksVisibility
        {
            get { return (Visibility)GetValue(BookmarksVisibilityProperty); }
            set { SetValue(BookmarksVisibilityProperty, value); }
        }
        public static readonly DependencyProperty BookmarksVisibilityProperty =
            DependencyProperty.Register("BookmarksVisibility", typeof(Visibility), typeof(Book), new UIPropertyMetadata(Visibility.Visible, OnBookmarksVisibilityPropertyChanged));

        #region Sheets

        public BookPage SheetLeft
        {
            get { return (BookPage)GetValue(SheetLeftProperty); }
            private set { SetValue(SheetLeftPropertyKey, value); }
        }

        public BookPage SheetRight
        {
            get { return (BookPage)GetValue(SheetRightProperty); }
            private set { SetValue(SheetRightPropertyKey, value); }
        }

        private static readonly DependencyPropertyKey SheetLeftPropertyKey = DependencyProperty.RegisterReadOnly(
            "SheetLeft", typeof(BookPage), typeof(Book), new FrameworkPropertyMetadata(null));
        public static readonly DependencyProperty SheetLeftProperty = SheetLeftPropertyKey.DependencyProperty;

        private static readonly DependencyPropertyKey SheetRightPropertyKey = DependencyProperty.RegisterReadOnly(
            "SheetRight", typeof(BookPage), typeof(Book), new FrameworkPropertyMetadata(null));
        public static readonly DependencyProperty SheetRightProperty = SheetRightPropertyKey.DependencyProperty;

        #endregion

        #region Styles & templates

        public DataTemplate ContentTemplate
        {
            get { return (DataTemplate)base.GetValue(ContentTemplateProperty); }
            set { base.SetValue(ContentTemplateProperty, value); }
        }

        public static readonly DependencyProperty ContentTemplateProperty =
            DependencyProperty.Register("ContentTemplate", typeof(DataTemplate), typeof(Book), new FrameworkPropertyMetadata(null));

        public DataTemplateSelector ContentTemplateSelector
        {
            get { return (DataTemplateSelector)base.GetValue(ContentTemplateSelectorProperty); }
            set { base.SetValue(ContentTemplateSelectorProperty, value); }
        }

        public static readonly DependencyProperty ContentTemplateSelectorProperty =
            DependencyProperty.Register("ContentTemplateSelector", typeof(DataTemplateSelector), typeof(Book), new FrameworkPropertyMetadata(null));

        public string ContentStringFormat
        {
            get { return (string)base.GetValue(ContentStringFormatProperty); }
            set { base.SetValue(ContentStringFormatProperty, value); }
        }
        public static readonly DependencyProperty ContentStringFormatProperty =
            DependencyProperty.Register("ContentStringFormat", typeof(string), typeof(Book), new FrameworkPropertyMetadata(null));


        public Style BookmarkerStyle
        {
            get { return (Style)GetValue(BookmarkerStyleProperty); }
            set { SetValue(BookmarkerStyleProperty, value); }
        }

        public static readonly DependencyProperty BookmarkerStyleProperty =
            DependencyProperty.Register("BookmarkerStyle", typeof(Style), typeof(Book), new UIPropertyMetadata(null));

        #endregion

        #region Bookmarks

        public Dock BookmarksPlacement
        {
            get { return (Dock)GetValue(BookmarksPlacementProperty); }
            set { SetValue(BookmarksPlacementProperty, value); }
        }

        public static readonly DependencyProperty BookmarksPlacementProperty =
            DependencyProperty.Register("BookmarksPlacement", typeof(Dock), typeof(Book),
            new FrameworkPropertyMetadata(Dock.Top, new PropertyChangedCallback(OnBookmarksPlacementPropertyChanged)));

        /// <summary>
        /// The bookmarker placement. Can't be Top or Bottom.
        /// </summary>
        public Dock BookmarkerPlacement
        {
            get { return (Dock)GetValue(BookmarkerPlacementProperty); }
            set { SetValue(BookmarkerPlacementProperty, value); }
        }

        public static readonly DependencyProperty BookmarkerPlacementProperty =
            DependencyProperty.Register("BookmarkerPlacement", typeof(Dock), typeof(Book), new UIPropertyMetadata(Dock.Right, null, CoerceBookmarkerPlacement));


        #endregion

        public int CurrentSheetIndex
        {
            get { return _currentSheetIndex; }
            private set
            {
                if (_status != PageStatus.None) return;

                if (_currentSheetIndex != value)
                {
                    if ((value >= 0) && (value <= Items.Count / 2))
                        _currentSheetIndex = value;
                    else
                        throw new Exception("Index out of bounds");
                }
            }
        }

        public BookDisplayMode DisplayMode
        {
            get { return (BookDisplayMode)GetValue(Book.DisplayModeProperty); }
            set
            {
                if (!GetValue(Book.DisplayModeProperty).Equals(value))
                    SetValue(Book.DisplayModeProperty, value);
            }
        }

        public BookCurrentPage CurrentPage
        {
            get { return (BookCurrentPage)GetValue(Book.CurrentPageProperty); }
            private set
            {
                if (!CurrentPage.Equals(value))
                    SetValue(Book.CurrentPageProperty, value);
            }
        }

        /// <summary>
        /// The grip size to drag a page
        /// </summary>
        public double GripSize
        {
            get { return (double)GetValue(GripSizeProperty); }
            set { SetValue(GripSizeProperty, value); }
        }

        public static readonly DependencyProperty GripSizeProperty =
            DependencyProperty.Register("GripSize", typeof(double), typeof(Book),
            new UIPropertyMetadata(Book.DefaultGripSize, OnGripSizePropertyChanged, OnCoerceGripSizePropertyChanged));


        #endregion

        #region Events

        public static readonly RoutedEvent BookmarkAddedEvent = EventManager.RegisterRoutedEvent(
            "BookmarkAdded", RoutingStrategy.Bubble, typeof(BookmarkAddedEventHandler), typeof(Book));

        public event BookmarkAddedEventHandler BookmarkAdded
        {
            add
            {
                base.AddHandler(BookmarkAddedEvent, value);
            }
            remove
            {
                base.RemoveHandler(BookmarkAddedEvent, value);
            }
        }

        #endregion

        #region Overrides

        protected override void OnInitialized(EventArgs e)
        {
            ItemContainerGenerator.StatusChanged += new EventHandler(OnGeneratorStatusChanged);

            base.OnInitialized(e);
        }

        private void OnGeneratorStatusChanged(object sender, EventArgs e)
        {
            if (base.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                if (HasItems && (SelectedIndex == -1))
                    SelectedIndex = 0;
                this.RefreshSheetsContent();
            }
        }

        public override void OnApplyTemplate()
        {

            //if (_bookmarker != null)
            //    Contacts.RemoveContactHoldGestureHandler(_bookmarker, OnBookmarkerContactHoldGesture);

            if (SheetLeft != null)
            {
                Contacts.RemovePreviewContactDownHandler(SheetLeft, OnPreviewLeftContactDown);
                SheetLeft.MouseDown -= OnLeftMouseDown;
                SheetLeft.PageTurned -= OnLeftPageTurned;
            }

            if (SheetRight != null)
            {
                Contacts.RemovePreviewContactDownHandler(SheetRight, OnPreviewRightContactDown);
                SheetRight.MouseDown -= OnRightMouseDown;
                SheetRight.PageTurned -= OnRightPageTurned;
            }

            _bookmarker = GetTemplateChild(Book.ElementBookmarker) as ContentControl;
            _sheetLeftPresenter = GetTemplateChild(Book.ElementSheetLeft) as ContentPresenter;
            _sheetRightPresenter = GetTemplateChild(Book.ElementSheetRight) as ContentPresenter;
            _rootGrid = GetTemplateChild(Book.ElementRoot) as Grid;
            _bookmarksSV = GetTemplateChild(Book.ElementBookmarksScrollViewer) as SurfaceScrollViewer;

            _scale = (_rootGrid.RenderTransform as TransformGroup).Children.OfType<ScaleTransform>().First();
            _translate = (_rootGrid.RenderTransform as TransformGroup).Children.OfType<TranslateTransform>().First();

            if (_bookmarker != null)
            {
                Contacts.AddContactDownHandler(_bookmarker, OnBookmarkerContactDown);
                Contacts.AddContactChangedHandler(_bookmarker, OnBookmarkerContactChanged);
                Contacts.AddContactUpHandler(_bookmarker, OnBookmarkerContactUp);
            }

            InitializeSheets();

            base.OnApplyTemplate();
        }


        private void InitializeSheets()
        {
            SheetLeft = new BookPage() { IsTopRightCornerEnabled = false, IsBottomRightCornerEnabled = false, GripSize = GripSize };
            SheetRight = new BookPage() { IsTopLeftCornerEnabled = false, IsBottomLeftCornerEnabled = false, GripSize = GripSize };

            if (PageSize != null)
                ApplyPageSizes(PageSize);

            Read<PageStatus> GetStatus = delegate() { return _status; };
            Action<PageStatus> SetStatus = delegate(PageStatus ps)
            {
                _status = ps;

                var busy = ps != PageStatus.None;
                SheetLeft.IsBookBusy = busy;
                SheetRight.IsBookBusy = busy;
            };

            SheetLeft.GetStatus += GetStatus;
            SheetLeft.SetStatus += SetStatus;
            SheetRight.GetStatus += GetStatus;
            SheetRight.SetStatus += SetStatus;

            // add handlers 
            SheetLeft.MouseDown += OnLeftMouseDown;
            SheetRight.MouseDown += OnRightMouseDown;

            SheetLeft.PageTurned += OnLeftPageTurned;
            SheetRight.PageTurned += OnRightPageTurned;

            Contacts.AddPreviewContactDownHandler(SheetLeft, OnPreviewLeftContactDown);
            Contacts.AddPreviewContactDownHandler(SheetRight, OnPreviewRightContactDown);
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            return new BookItem();
        }

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return (item is BookItem);
        }

        #endregion

        #region Private helpers

        private BookItem GetBookItem(int index)
        {
            if (index >= Items.Count || index < 0)
                return null;

            BookItem item = Items[index] as BookItem;

            if (item == null)
                item = base.ItemContainerGenerator.ContainerFromIndex(index) as BookItem;

            return item;
        }

        private object GetPageContent(int index)
        {
            if ((index >= 0) && (index < Items.Count))
                return GetBookItem(index).Content;

            else
                return new Canvas();
        }

        private int GetLeftPageIndexForIndex(int index)
        {
            return index % 2 == 0 ?
                index - 1 : index;
        }

        private int GetRightPageIndexForIndex(int index)
        {
            return index % 2 == 0 ?
                index : index + 1;
        }

        #endregion

        private static void OnGripSizePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var book = d as Book;

            if (book == null)
                return;

            book.SheetLeft.GripSize = (double)e.NewValue;
            book.SheetRight.GripSize = (double)e.NewValue;
        }

        private static object OnCoerceGripSizePropertyChanged(DependencyObject d, object baseValue)
        {
            if ((double)baseValue < 0)
                return 0d;
            else
                return baseValue;
        }

        private static void OnBookmarksPlacementPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Book control = (Book)d;
            ItemCollection items = control.Items;
            for (int i = 0; i < items.Count; i++)
            {
                BookItem item = control.ItemContainerGenerator.ContainerFromIndex(i) as BookItem;
                if (item != null)
                    item.CoerceValue(BookItem.BookmarksPlacementProperty);
            }

            control.RefreshSheetsContent();
        }

        private static void OnBookmarksVisibilityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Book control = (Book)d;
            ItemCollection items = control.Items;
            for (int i = 0; i < items.Count; i++)
            {
                BookItem item = control.ItemContainerGenerator.ContainerFromIndex(i) as BookItem;
                if (item != null)
                    item.CoerceValue(BookItem.BookmarksVisibilityProperty);
            }
        }

        private static void OnPageSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Book control = (Book)d;

            var newSize = (Size?)e.NewValue;

            control.ApplyPageSizes(newSize);
        }

        private void ApplyPageSizes(Size? size)
        {
            if (size != null)
            {
                if (this.SheetLeft != null)
                {
                    this.SheetLeft.Width = size.Value.Width;
                    this.SheetLeft.Height = size.Value.Height;
                }
                if (this.SheetRight != null)
                {
                    this.SheetRight.Width = size.Value.Width;
                    this.SheetRight.Height = size.Value.Height;
                }

                if (_bookmarksSV != null)
                {
                    _bookmarksSV.MaxWidth = size.Value.Width * 2;
                    _bookmarksSV.MaxHeight = size.Value.Height;
                }
            }
            else
            {
                if (this.SheetLeft != null)
                {
                    this.SheetLeft.Width = double.NaN;
                    this.SheetLeft.Height = double.NaN;
                }
                if (this.SheetRight != null)
                {
                    this.SheetRight.Width = double.NaN;
                    this.SheetRight.Height = double.NaN;
                }

                if (_bookmarksSV != null)
                {
                    _bookmarksSV.MaxWidth = double.NaN;
                    _bookmarksSV.MaxHeight = double.NaN;
                }
            }
        }

        private static void OnDisplayModeChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
        {
            BookDisplayMode mode = (BookDisplayMode)args.NewValue;
            Book book = source as Book;
            if (mode == BookDisplayMode.Normal)
            {
                book._translate.BeginAnimation(TranslateTransform.XProperty, null);
                book._scale.BeginAnimation(ScaleTransform.ScaleXProperty, null);
            }
            else
            {
                if (book.CurrentPage == BookCurrentPage.LeftSheet)
                    book.AnimateToLeftSheet();
                else
                    book.AnimateToRightSheet();
            }
        }

        private static void OnCurrentPageChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
        {
            BookCurrentPage currentPage = (BookCurrentPage)args.NewValue;
            Book b = source as Book;

            if (b.DisplayMode == BookDisplayMode.ZoomOnPage)
            {
                if (currentPage == BookCurrentPage.LeftSheet)
                    b.AnimateToLeftSheet();
                else
                    b.AnimateToRightSheet();
            }
        }

        private static object CoerceBookmarkerPlacement(DependencyObject d, object baseValue)
        {
            var dock = (Dock)baseValue;
            if (dock == Dock.Bottom || dock == Dock.Top)
                dock = Dock.Right;

            return dock;
        }

        private Point? _lastPosition;
        private Storyboard _bookmarkerStoryboard;

        private void OnBookmarkerContactDown(object sender, ContactEventArgs e)
        {
            Canvas.SetZIndex(_bookmarker, 1);

            // if there is already a contact
            if (_lastPosition != null)
            {
                // don't do anything
            }
            else
            {
                e.Contact.Capture(_bookmarker);
                _lastPosition = e.Contact.GetPosition(this);

                if (_bookmarkerStoryboard != null)
                    _bookmarkerStoryboard.Remove(_bookmarker);
            }


            e.Handled = true;
        }

        private void OnBookmarkerContactChanged(object sender, ContactEventArgs e)
        {
            if (e.Contact.Captured == _bookmarker)
            {
                var trans = this._bookmarker.RenderTransform as TranslateTransform;

                if (trans == null)
                {
                    this._bookmarker.RenderTransform = new TranslateTransform();
                    trans = this._bookmarker.RenderTransform as TranslateTransform;
                }

                var newPosition = e.Contact.GetPosition(this);

                if (_lastPosition != null)
                {
                    Vector delta = newPosition - _lastPosition.Value;
                    trans.X += delta.X;
                    trans.Y += delta.Y;
                }

                _lastPosition = newPosition;

            }

            e.Handled = true;
        }

        private void OnBookmarkerContactUp(object sender, ContactEventArgs e)
        {
            var otherContacts = Contacts.GetContactsOver(_bookmarker);

            if (otherContacts.Count > 1)
            {
                // capture the first other contact 
                var capturingContact = otherContacts.Where(c => c != e.Contact).First();

                capturingContact.Capture(_bookmarker);
                _lastPosition = capturingContact.GetPosition(this);
            }
            else
            {

                e.Contact.Capture(null);
                _lastPosition = null;

                // animate it to its origin
                var trans = this._bookmarker.RenderTransform as TranslateTransform;

                var startPoint = new Point(trans.X, trans.Y);

                // detect if dropped on a Sheet
                var pointBookmarker = _bookmarker.TranslatePoint(new Point(_bookmarker.ActualWidth / 2, 0), this);

                var rectSheetLeft = new Rect(SheetLeft.TranslatePoint(new Point(), this), new Size(SheetLeft.ActualWidth, SheetRight.ActualHeight));
                var rectSheetRight = new Rect(SheetRight.TranslatePoint(new Point(), this), new Size(SheetRight.ActualWidth, SheetRight.ActualHeight));

                _bookmarkerStoryboard = new Storyboard();

                if (rectSheetLeft.Contains(pointBookmarker))
                {
                    var bi = this.GetBookItem(this.LeftPageIndex);

                    if (bi != null && bi.Header == null)
                    {
                        BookmarkAddedEventArgs args = new BookmarkAddedEventArgs
                        {
                            BookItem = bi,
                            CurrentPage = BookCurrentPage.LeftSheet,
                            PageIndex = this.LeftPageIndex
                        };

                        OnBookmarkAdded(args);

                        Dispatcher.BeginInvoke((Action)(() =>
                            {
                                CreateBookmarkerAnimationDroppedOnSheet(trans, startPoint, bi);
                                StartBookmarkerAnimation(trans);
                            }), System.Windows.Threading.DispatcherPriority.Background, null);

                    }
                    else
                    {
                        CreateBookmarkerAnimationGoBackToOrigin(startPoint);
                        StartBookmarkerAnimation(trans);
                    }
                }
                else if (rectSheetRight.Contains(pointBookmarker))
                {
                    var bi = this.GetBookItem(this.RightPageIndex);

                    if (bi != null && bi.Header == null)
                    {
                        BookmarkAddedEventArgs args = new BookmarkAddedEventArgs
                        {
                            BookItem = bi,
                            CurrentPage = BookCurrentPage.RightSheet,
                            PageIndex = this.RightPageIndex
                        };

                        OnBookmarkAdded(args);

                        Dispatcher.BeginInvoke((Action)(() =>
                        {
                            CreateBookmarkerAnimationDroppedOnSheet(trans, startPoint, bi);
                            StartBookmarkerAnimation(trans);
                        }), System.Windows.Threading.DispatcherPriority.Background, null);
                    }
                    else
                    {
                        CreateBookmarkerAnimationGoBackToOrigin(startPoint);
                        StartBookmarkerAnimation(trans);
                    }
                }
                else
                {
                    CreateBookmarkerAnimationGoBackToOrigin(startPoint);
                    StartBookmarkerAnimation(trans);
                }

            }

            e.Handled = true;
        }

        private void CreateBookmarkerAnimationGoBackToOrigin(Point startPoint)
        {
            BezierSegment bs =
                new BezierSegment(startPoint, new Point(startPoint.X / 2, 0), new Point(0, 0), true);

            PathGeometry path = new PathGeometry();
            PathFigure figure = new PathFigure();
            figure.StartPoint = startPoint;
            figure.Segments.Add(bs);
            figure.IsClosed = false;
            path.Figures.Add(figure);

            DoubleAnimationUsingPath animX = new DoubleAnimationUsingPath()
            {
                Source = PathAnimationSource.X,
                PathGeometry = path,
                Duration = BookmarkerTranslateDuration,
                AccelerationRatio = .6,
                DecelerationRatio = .4,
            };

            DoubleAnimationUsingPath animY = animX.Clone();
            animY.Source = PathAnimationSource.Y;

            _bookmarkerStoryboard.Children.Add(animX);
            _bookmarkerStoryboard.Children.Add(animY);

            Storyboard.SetTargetProperty(animX, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.X)"));
            Storyboard.SetTargetProperty(animY, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)"));
        }

        private void CreateBookmarkerAnimationDroppedOnSheet(TranslateTransform trans, Point startPoint, BookItem bi)
        {
            Point posForBookmark = bi.TranslatePoint(new Point(- (_bookmarker.ActualWidth - bi.ActualWidth) /2, 0), _bookmarker) 
                + new Vector(trans.X, trans.Y);

            BezierSegment bs =
                new BezierSegment(startPoint, new Point(startPoint.X + (posForBookmark.X - startPoint.X) / 2, 0), posForBookmark, true);

            PathGeometry path = new PathGeometry();
            PathFigure figure = new PathFigure();
            figure.StartPoint = startPoint;
            figure.Segments.Add(bs);
            figure.IsClosed = false;
            path.Figures.Add(figure);

            DoubleAnimationUsingPath animX = new DoubleAnimationUsingPath()
            {
                Source = PathAnimationSource.X,
                PathGeometry = path,
                Duration = BookmarkerTranslateDuration,
                AccelerationRatio = .6,
                DecelerationRatio = .4,
            };

            DoubleAnimationUsingPath animY = animX.Clone();
            animY.Source = PathAnimationSource.Y;

            DoubleAnimation animOpacity = new DoubleAnimation(0, BookmarkerFadeDuration);
            animOpacity.BeginTime = BookmarkerTranslateDuration.TimeSpan;

            DoubleAnimation animBackToOriginX = new DoubleAnimation(0, new Duration(TimeSpan.FromMilliseconds(0)));
            animBackToOriginX.BeginTime = BookmarkerTranslateDuration.TimeSpan + BookmarkerFadeDuration.TimeSpan;
            DoubleAnimation animBackToOriginY = animBackToOriginX.Clone();

            DoubleAnimation animBackOpacity = new DoubleAnimation(1, BookmarkerFadeDuration);
            animBackOpacity.BeginTime = BookmarkerTranslateDuration.TimeSpan + BookmarkerFadeDuration.TimeSpan;

            _bookmarkerStoryboard.Children.Add(animX);
            _bookmarkerStoryboard.Children.Add(animY);
            _bookmarkerStoryboard.Children.Add(animOpacity);
            _bookmarkerStoryboard.Children.Add(animBackToOriginX);
            _bookmarkerStoryboard.Children.Add(animBackToOriginY);
            _bookmarkerStoryboard.Children.Add(animBackOpacity);

            Storyboard.SetTargetProperty(animX, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.X)"));
            Storyboard.SetTargetProperty(animY, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)"));
            Storyboard.SetTargetProperty(animOpacity, new PropertyPath("Opacity"));
            Storyboard.SetTargetProperty(animBackToOriginX, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.X)"));
            Storyboard.SetTargetProperty(animBackToOriginY, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)"));
            Storyboard.SetTargetProperty(animBackOpacity, new PropertyPath("Opacity"));
        }

        private void StartBookmarkerAnimation(TranslateTransform trans)
        {
            _bookmarkerStoryboard.RemoveRequested += (s, ee) =>
            {
                // apply the values before stopping animation
                trans.X = trans.X;
                trans.Y = trans.Y;

                _bookmarkerStoryboard = null;
            };

            _bookmarkerStoryboard.Completed += (s, ee) =>
            {
                _bookmarkerStoryboard.Remove(_bookmarker);
            };

            _bookmarkerStoryboard.Begin(_bookmarker, true);
        }

        protected virtual void OnBookmarkAdded(BookmarkAddedEventArgs e)
        {
            e.RoutedEvent = BookmarkAddedEvent;

            RaiseEvent(e);
            if (!e.IsHeaderHandled)
            {
                // apply default bookmark
                e.BookItem.Header = "Page " + e.PageIndex;

                // launch edit in background in order to do it only after Template are applied
                Dispatcher.BeginInvoke((Action)(() => e.BookItem.BeginEdit()), System.Windows.Threading.DispatcherPriority.Background, null);

            }
        }

        private void OnLoaded(object sender, RoutedEventArgs args)
        {
            RefreshSheetsContent();
        }

        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            if (_status != PageStatus.None)
            {
                SheetLeft.StopAllAnimations();
                SheetRight.StopAllAnimations();
            }

            base.OnSelectionChanged(e);

            // check for CurrentSheetIndex and CurrentPage
            BookCurrentPage page = this.SelectedIndex % 2 == 0 ? BookCurrentPage.RightSheet : BookCurrentPage.LeftSheet;
            int sheetIndex = (this.SelectedIndex + 1) / 2;

            if (CurrentSheetIndex != sheetIndex)
                CurrentSheetIndex = sheetIndex;

            RefreshSheetsContent();
            RefreshSelectedBookmarks();

            // focus the TabItem to it in the ScrollViewer
            var tabItem = GetBookItem(SelectedIndex);
            if (tabItem != null)
                tabItem.Focus();

            if (CurrentPage != page)
                this.CurrentPage = page;
        }

        private void RefreshSelectedBookmarks()
        {
            for (int i = 0; i < this.Items.Count; i++)
            {
                var item = GetBookItem(i);
                if (item != null)
                    item.IsSelected = i == LeftPageIndex || i == RightPageIndex;
            }
        }

        protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);
            if (CheckCurrentSheetIndex())
                SelectedIndex = this.Items.Count - 1;
        }

        protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
        {
            base.OnItemsSourceChanged(oldValue, newValue);
            if (CheckCurrentSheetIndex())
                SelectedIndex = this.Items.Count - 1;
        }

        protected virtual bool CheckCurrentSheetIndex()
        {
            return CurrentSheetIndex > (Items.Count / 2);
        }

        private void RefreshSheetsContent()
        {
            if (SheetLeft == null || SheetRight == null)
                return;

            Visibility bp0Visibility = Visibility.Visible;
            Visibility bp1Visibility = Visibility.Visible;

            SheetRight.IsTopRightCornerEnabled = true;
            SheetRight.IsBottomRightCornerEnabled = true;

            Visibility sheet0Page0ContentVisibility = Visibility.Visible;
            Visibility sheet0Page1ContentVisibility = Visibility.Visible;
            Visibility sheet0Page2ContentVisibility = Visibility.Visible;
            Visibility sheet1Page0ContentVisibility = Visibility.Visible;
            Visibility sheet1Page1ContentVisibility = Visibility.Visible;
            Visibility sheet1Page2ContentVisibility = Visibility.Visible;

            SetContentTemplates();

            sheet0Page2ContentVisibility = CurrentSheetIndex == 1 ? Visibility.Hidden : Visibility.Visible;
            int count = Items.Count;
            int sheetCount = count / 2;
            bool isOdd = (count % 2) == 1;

            if (CurrentSheetIndex == sheetCount)
            {
                if (isOdd)
                {
                    SheetRight.IsTopRightCornerEnabled = false;
                    SheetRight.IsBottomRightCornerEnabled = false;
                }
                else
                    bp1Visibility = Visibility.Hidden;
            }

            if (CurrentSheetIndex == sheetCount - 1)
            {
                if (!isOdd)
                    sheet1Page2ContentVisibility = Visibility.Hidden;
            }

            if (CurrentSheetIndex == 0)
            {
                SheetLeft.Page0Content = null;
                SheetLeft.Page0ContentTemplate = null;
                SheetLeft.Page0ContentTemplateSelector = null;
                SheetLeft.Page0ContentStringFormat = null;

                SheetLeft.Page1Content = null;
                SheetLeft.Page1ContentTemplate = null;
                SheetLeft.Page1ContentTemplateSelector = null;
                SheetLeft.Page1ContentStringFormat = null;

                SheetLeft.Page2Content = null;
                SheetLeft.Page2ContentTemplate = null;
                SheetLeft.Page2ContentTemplateSelector = null;
                SheetLeft.Page2ContentStringFormat = null;

                SheetLeft.IsEnabled = false;
                bp0Visibility = Visibility.Hidden;
            }
            else
            {

                SheetLeft.Page0Content = GetPageContent(2 * (CurrentSheetIndex - 1) + 1);
                SheetLeft.Page1Content = GetPageContent(2 * (CurrentSheetIndex - 1));
                SheetLeft.Page2Content = GetPageContent(2 * (CurrentSheetIndex - 1) - 1);

                SheetLeft.IsEnabled = true;
            }

            SheetRight.Page0Content = GetPageContent(2 * CurrentSheetIndex);
            SheetRight.Page1Content = GetPageContent(2 * CurrentSheetIndex + 1);
            SheetRight.Page2Content = GetPageContent(2 * CurrentSheetIndex + 2);

            SheetLeft.Visibility = bp0Visibility;
            SheetRight.Visibility = bp1Visibility;

            SheetLeft.SetPage0PresenterVisibility(sheet0Page0ContentVisibility);
            SheetLeft.SetPage1PresenterVisibility(sheet0Page1ContentVisibility);
            SheetLeft.SetPage2PresenterVisibility(sheet0Page2ContentVisibility);
            SheetRight.SetPage0PresenterVisibility(sheet1Page0ContentVisibility);
            SheetRight.SetPage1PresenterVisibility(sheet1Page1ContentVisibility);
            SheetRight.SetPage2PresenterVisibility(sheet1Page2ContentVisibility);

        }

        private void SetContentTemplates()
        {
            if (ContentTemplate != null)
            {
                if (SheetLeft.Page0ContentTemplate == null)
                    SheetLeft.Page0ContentTemplate = ContentTemplate;
                if (SheetLeft.Page1ContentTemplate == null)
                    SheetLeft.Page1ContentTemplate = ContentTemplate;
                if (SheetLeft.Page2ContentTemplate == null)
                    SheetLeft.Page2ContentTemplate = ContentTemplate;
                if (SheetRight.Page0ContentTemplate == null)
                    SheetRight.Page0ContentTemplate = ContentTemplate;
                if (SheetRight.Page1ContentTemplate == null)
                    SheetRight.Page1ContentTemplate = ContentTemplate;
                if (SheetRight.Page2ContentTemplate == null)
                    SheetRight.Page2ContentTemplate = ContentTemplate;
            }

            if (ContentTemplateSelector != null)
            {
                if (SheetLeft.Page0ContentTemplateSelector == null)
                    SheetLeft.Page0ContentTemplateSelector = ContentTemplateSelector;
                if (SheetLeft.Page1ContentTemplateSelector == null)
                    SheetLeft.Page1ContentTemplateSelector = ContentTemplateSelector;
                if (SheetLeft.Page2ContentTemplateSelector == null)
                    SheetLeft.Page2ContentTemplateSelector = ContentTemplateSelector;
                if (SheetRight.Page0ContentTemplateSelector == null)
                    SheetRight.Page0ContentTemplateSelector = ContentTemplateSelector;
                if (SheetRight.Page1ContentTemplateSelector == null)
                    SheetRight.Page1ContentTemplateSelector = ContentTemplateSelector;
                if (SheetRight.Page2ContentTemplateSelector == null)
                    SheetRight.Page2ContentTemplateSelector = ContentTemplateSelector;
            }

            if (ContentStringFormat != null)
            {
                if (SheetLeft.Page0ContentStringFormat == null)
                    SheetLeft.Page0ContentStringFormat = ContentStringFormat;
                if (SheetLeft.Page1ContentStringFormat == null)
                    SheetLeft.Page1ContentStringFormat = ContentStringFormat;
                if (SheetLeft.Page2ContentStringFormat == null)
                    SheetLeft.Page2ContentStringFormat = ContentStringFormat;
                if (SheetRight.Page0ContentStringFormat == null)
                    SheetRight.Page0ContentStringFormat = ContentStringFormat;
                if (SheetRight.Page1ContentStringFormat == null)
                    SheetRight.Page1ContentStringFormat = ContentStringFormat;
                if (SheetRight.Page2ContentStringFormat == null)
                    SheetRight.Page2ContentStringFormat = ContentStringFormat;
            }
        }

        private void OnLeftMouseDown(object sender, MouseButtonEventArgs args)
        {
            SetPageForeground(BookCurrentPage.LeftSheet);
        }
        private void OnRightMouseDown(object sender, MouseButtonEventArgs args)
        {
            SetPageForeground(BookCurrentPage.RightSheet);
        }

        /// <summary>
        /// Sets the right page foreground
        /// </summary>
        /// <param name="right">Either right or left page</param>
        private void SetPageForeground(BookCurrentPage side)
        {
            Canvas.SetZIndex(_sheetLeftPresenter, side == BookCurrentPage.LeftSheet ? 1 : 0);
            Canvas.SetZIndex(_sheetRightPresenter, side == BookCurrentPage.RightSheet ? 1 : 0);
        }

        #region Surface

        private void OnPreviewLeftContactDown(object sender, ContactEventArgs e)
        {
            // set only if doing nothing
            if (_status == PageStatus.None)
                SetPageForeground(BookCurrentPage.LeftSheet);
        }

        private void OnPreviewRightContactDown(object sender, ContactEventArgs e)
        {
            // set only if doing nothing
            if (_status == PageStatus.None)
                SetPageForeground(BookCurrentPage.RightSheet);
        }

        #endregion

        private void OnLeftPageTurned(object sender, RoutedEventArgs args)
        {
            this.SelectedIndex = LeftPageIndex - 1;

        }

        private void OnRightPageTurned(object sender, RoutedEventArgs args)
        {
            this.SelectedIndex = RightPageIndex + 1;
        }

        private void AnimateToLeftSheet()
        {
            DoubleAnimation translateAnimation = new DoubleAnimation(ActualWidth / 4, new Duration(TimeSpan.FromMilliseconds(500)));

            BezierSegment bs =
                new BezierSegment(new Point(0, 1), new Point(1, 0.5), new Point(2, 1), true);

            PathGeometry path = new PathGeometry();
            PathFigure figure = new PathFigure();
            figure.StartPoint = new Point(0, 1);
            figure.Segments.Add(bs);
            figure.IsClosed = false;
            path.Figures.Add(figure);

            DoubleAnimationUsingPath scaleAnimation = new DoubleAnimationUsingPath();
            scaleAnimation.PathGeometry = path;
            scaleAnimation.Source = PathAnimationSource.Y;
            scaleAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(500));

            _translate.BeginAnimation(TranslateTransform.XProperty, translateAnimation);
            _scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation);
            _scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation);
        }

        private void AnimateToRightSheet()
        {
            DoubleAnimation translateAnimation = new DoubleAnimation(-ActualWidth / 4, new Duration(TimeSpan.FromMilliseconds(500)));

            BezierSegment bs =
                new BezierSegment(new Point(0, 1), new Point(1, 0.5), new Point(2, 1), true);

            PathGeometry path = new PathGeometry();
            PathFigure figure = new PathFigure();
            figure.StartPoint = new Point(0, 1);
            figure.Segments.Add(bs);
            figure.IsClosed = false;
            path.Figures.Add(figure);

            DoubleAnimationUsingPath scaleAnimation = new DoubleAnimationUsingPath();
            scaleAnimation.PathGeometry = path;
            scaleAnimation.Source = PathAnimationSource.Y;
            scaleAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(500));

            _translate.BeginAnimation(TranslateTransform.XProperty, translateAnimation);
            _scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation);
            _scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation);
        }

        public void MoveToNextPage()
        {
            if (SelectedIndex + 1 < Items.Count)
                SelectedIndex += 1;
        }

        public void MoveToPreviousPage()
        {
            if (SelectedIndex > 0)
                SelectedIndex -= 1;
        }

        public void MoveToNextSheet()
        {
            if (RightPageIndex + 1 >= Items.Count)
                SelectedIndex = RightPageIndex;
            else
                SelectedIndex = RightPageIndex + 1;
        }

        public void MoveToPreviousSheet()
        {
            if (LeftPageIndex - 1 < 0)
                SelectedIndex = LeftPageIndex;
            else
                SelectedIndex = LeftPageIndex - 1;
        }

        public void AnimateToIndex(int index)
        {
            AnimateToIndex(index, 500, true);
        }

        public void AnimateToIndex(int index, int duration)
        {
            AnimateToIndex(index, duration, true);
        }

        public void AnimateToIndex(int index, int duration, bool fromTop)
        {
            if (index < 0 || index >= Items.Count)
                return;

            if (index == LeftPageIndex || index == RightPageIndex)
            {
                // just change selected index
                SelectedIndex = index;
            }
            else
            {
                if (index < LeftPageIndex)
                {
                    if (index < LeftPageIndex - 2)
                    {
                        // load the pages
                        var leftIndex = GetLeftPageIndexForIndex(index);
                        var rightIndex = GetRightPageIndexForIndex(index);
                        SheetLeft.Page1Content = GetPageContent(rightIndex);
                        SheetLeft.Page2Content = GetPageContent(leftIndex - 1);
                    }

                    SetPageForeground(BookCurrentPage.LeftSheet);
                    SheetLeft.AutoTurnPage(fromTop ? CornerOrigin.TopLeft : CornerOrigin.BottomLeft, duration, OnTurnPageCompleted, index);
                }
                else
                {
                    if (index > RightPageIndex + 2)
                    {
                        // load the pages
                        var leftIndex = GetLeftPageIndexForIndex(index);
                        var rightIndex = GetRightPageIndexForIndex(index);
                        SheetRight.Page1Content = GetPageContent(leftIndex);
                        SheetRight.Page2Content = GetPageContent(rightIndex + 1);
                    }

                    SetPageForeground(BookCurrentPage.RightSheet);
                    SheetRight.AutoTurnPage(fromTop ? CornerOrigin.TopRight : CornerOrigin.BottomRight, duration, OnTurnPageCompleted, index);
                }
            }
        }

        internal void OnTurnPageCompleted(int index)
        {
            this.SelectedIndex = index;
        }
    }

    public enum BookDisplayMode { Normal, ZoomOnPage }
    public enum BookCurrentPage { LeftSheet, RightSheet }

    public delegate void BookmarkAddedEventHandler(object sender, BookmarkAddedEventArgs e);
}
